﻿/**

 * Copyright (c) 2015-2017, FastDev 刘强 (fastdev@163.com).

 *

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *      http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */


using Mono.Cecil;
using Mono.Cecil.Cil;
using NLog;
using OF.ILModifier.Entity;
using OF.ILModifier.Handler;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using OF.ILModifier.Extension;

namespace OF.ILModifier.Common
{
    public class MethodHelper
    {
        public static MethodReference MakeGenericMethod(MethodReference self, params TypeReference[] arguments)
        {
            if (self.GenericParameters.Count != arguments.Length)
            {
                throw new ArgumentException();
            }
            var instance = new GenericInstanceMethod(self);
            foreach (var argument in arguments)
            {
                instance.GenericArguments.Add(argument);
            }
            return instance;
        }

        public static void CloneGenericParameters(MethodReference reference, MethodDefinition target)
        {
            foreach (var param in target.GenericParameters)
            {
                reference.GenericParameters.Add(param);
            }
        }

        public static MethodReference GetCallMethod(MethodReference reference, MethodDefinition target)
        {
            MethodReference callMethod = null;
            if (target.GenericParameters.Count > 0)
            {
                callMethod = MethodHelper.MakeGenericMethod(reference, target.GenericParameters.Select(item => (TypeReference)item).ToArray());
            }
            else
            {
                callMethod = reference;
            }
            return callMethod;
        }

        public static MethodDefinition MoveMethod(TypeDefinition targetType, MethodDefinition sourceInvoker)
        {
            MethodDefinition movedInvoker = sourceInvoker.CopyEmptyBody(sourceInvoker.Name + DateTime.Now.ToString("yyyyMMddHHmmssfff"));
            targetType.Methods.Add(movedInvoker);
            sourceInvoker.SetVariables(sourceInvoker.Module, movedInvoker);

            ILProcessor oldProcessor = sourceInvoker.Body.GetILProcessor();
            ILProcessor rawProcessor = movedInvoker.Body.GetILProcessor();
            foreach (var ins in sourceInvoker.Body.Instructions.ToList())
            {
                rawProcessor.Append(ins);
                oldProcessor.Remove(ins);
            }
            return movedInvoker;
        }

        public static void AppendCallIns(ILProcessor pWorker, int paramsCount, List<Instruction> result, MethodReference referedMethod, MethodDefinition target)
        {
            if (referedMethod == null)
            {
                return;
            }
            
            for (int mI = 0; mI < paramsCount; mI++)
            {
                result.Add(GetLoadParamIns(pWorker, mI));
            }
            Instruction insCall = pWorker.Create(OpCodes.Call, MethodHelper.GetCallMethod(referedMethod, target));
            result.Add(insCall);
        }

        public static void AddStaticCall(AssemblyHelper asmHelper, ILProcessor pWorker, List<TypeReference> paramTypeList, 
            List<Instruction> result, MethodReference referedMethod)
        {
            if (referedMethod == null)
            {
                return;
            }
            Instruction insCall;
            insCall = pWorker.Create(OpCodes.Newobj, asmHelper.ParamContainerConstructor);
            result.Add(insCall);
            LoadParams(result, pWorker, paramTypeList, (insParam, paramType) => {
                result.Add(insParam);
                result.Add(pWorker.Create(OpCodes.Call, MethodHelper.MakeGenericMethod(asmHelper.ParamContainerAddMethod, new TypeReference[] { paramType })));
            });
            insCall = pWorker.Create(OpCodes.Call, referedMethod);
            result.Add(insCall);
        }

        private static Instruction GetLoadParamIns(ILProcessor pWorker, int mI)
        {
            Instruction insCall;
            if (mI == 0)
            {
                insCall = pWorker.Create(OpCodes.Ldarg_0);
            }
            else if (mI == 1)
            {
                insCall = pWorker.Create(OpCodes.Ldarg_1);
            }
            else if (mI == 2)
            {
                insCall = pWorker.Create(OpCodes.Ldarg_2);
            }
            else if (mI == 3)
            {
                insCall = pWorker.Create(OpCodes.Ldarg_3);
            }
            else
            {
                insCall = pWorker.Create(OpCodes.Ldarg_S, (byte)mI);
            }
            return insCall;
        }

        public static void LoadParams(List<Instruction> result, ILProcessor pWorker, List<TypeReference> paramTypeList, Action<Instruction, TypeReference> addInsAction)
        {
            for (int mI = 0; mI < paramTypeList.Count; mI++)
            {
                Instruction insCall = GetLoadParamIns(pWorker, mI);
                addInsAction(insCall, paramTypeList[mI]);
            }
        }

        public static void ReplaceCallIns(ILBuilder builder,
            MethodBase inMethod, MethodBase callMethod, MethodBase replaceActMethod)
        {
            if (!replaceActMethod.IsStatic)
            {
                throw new NotSupportedException("Replace method only support for static method!");
            }
            AssemblyHelper asm = builder.GetAsm();
            builder.SetTargetTypeFilter((type) => asm.IsEquals(type, inMethod.DeclaringType))
                .SetTargetMethodFilter((type, method) => asm.IsEquals(method, inMethod))
                .SetTargetInstructionFilter((type, method, ins) => ILHelper.IsCall(ins)
                    && asm.IsEquals((MethodReference)ins.Operand, callMethod))
                .SetAfterInsFunc((type, method, ins) =>
                {
                    ILProcessor worker = method.Body.GetILProcessor();
                    return new Instruction[] { worker.Create(OpCodes.Call, builder.GetAsm().ImportReference(replaceActMethod)) };
                })
                .SetRemoveInsFunc((type, method, ins) =>
                {
                    return true;
                })
                .Visit();
        }

        public static void InjectWrapEvents(ILBuilder builder, MethodInfo beforeMethod, MethodInfo rawTargetMethod, Func<MethodDefinition, MethodInfo> replaceMethod, MethodInfo afterMethod)
        {
            var rawTargetMethodDefine = ILHelper.ToDefinition(rawTargetMethod);
            builder.SetTargetTypeFilter((type) => {
                bool isOk = type.GetFullName() == rawTargetMethodDefine.DeclaringType.GetFullName();
                return isOk;
            })
           .SetTargetMethodFilter((targetType, targetMethod) => {
               bool isOk = targetMethod.GetFullName() == rawTargetMethodDefine.GetFullName();
               return isOk;
           }).SetInterceptMethods(beforeMethod, replaceMethod, afterMethod);
            builder.Visit();
        }

        public static void InjectProxy(ILBuilder builder, MethodInfo callMethod, Func<ParamContainer, ParamContainer> replaceFunc, Action<ParamContainer, Exception> onException)
        {
            AssemblyHelper asm = builder.GetAsm();
            builder.SetTargetTypeFilter((type) => asm.IsEquals(type, callMethod.DeclaringType))
                .SetTargetMethodFilter((type, method) =>
                {
                    bool isOk = asm.IsEquals(method, callMethod);
                    if (isOk)
                    {
                        new MethodReplaceHandler(builder.GetAsm()).ReplaceCommonDefinition(type, method, replaceFunc, onException);
                    }
                    return isOk;
                }).Visit();
        }

        public static List<string> GetMethodCodes(MethodInfo viewMethod, ILBuilder builder)
        {
            List<string> resultList = new List<string>();
            var asm = builder.GetAsm();
            builder.SetTargetTypeFilter((type) => asm.IsEquals(type, viewMethod.DeclaringType))
                .SetTargetMethodFilter((type, method) => asm.IsEquals(method, viewMethod))
                .SetTargetInstructionFilter((type, method, ins) => {
                    resultList.Add(ins.ToString());
                    return false;
                })
            .Visit();
            return resultList;
        }

        public static Instruction[] GetCallInstruction(AssemblyHelper asmHelper, MethodDefinition target, MethodInfo[] callArray)
        {
            Instruction[] result = new Instruction[callArray.Length];
            for (int mI = 0; mI < callArray.Length; mI++)
            {
                var m = callArray[mI];
                MethodReference refernce = asmHelper.ImportReference(m);
                ILProcessor worker = target.Body.GetILProcessor();
                Instruction insCall = worker.Create(OpCodes.Call, refernce);
                result[mI] = insCall;
            }
            return result;
        }
    }
}



/*
if (mI == 0)
{
    insCall = pWorker.Create(OpCodes.Ldarg_0);
}
else if (mI == 1)
{
    insCall = pWorker.Create(OpCodes.Ldarg_1);
}
else if (mI == 2)
{
    insCall = pWorker.Create(OpCodes.Ldarg_2);
}
else if (mI == 3)
{
    insCall = pWorker.Create(OpCodes.Ldarg_3);
}
else
{
    insCall = pWorker.Create(OpCodes.Ldarg_S, (byte)mI);
}
*/
